﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Collections;
using System.Collections.ObjectModel;

namespace Barbar.UrlGuard.Dal.Persist {
    /// <summary>
    /// A binary record file
    /// </summary>
  public class RecordFile : IDisposable {
    public const int DEFAULT_CHUNK_SIZE = 4000;
    private const byte TRANSACTION_BYTE = 84;
    private const byte COMMIT_BYTE = 67;

    // The underlying binary filestream
    private FileStream m_FileStream;
    // Record header information by monitor ID
    private Dictionary<Guid, RecordHeader> m_StartChunks = new Dictionary<Guid, RecordHeader>();
    // Special ID used to denote the table of contents that contains all the other record headers
    private static readonly Guid RecordContentsHeaderID = Guid.Empty;
    // Binary serializers
    private IBinaryFactory<RecordHeader> m_HeaderFactory;
    private IBinaryFactory<IndexEntry> m_IndexFactory;

    public RecordFile(string fileName, IBinaryFactory<RecordHeader> headerFactory, IBinaryFactory<IndexEntry> indexFactory) {
      if (string.IsNullOrEmpty(fileName))
        throw new ArgumentNullException("fileName");

      if (headerFactory == null)
        throw new ArgumentNullException("headerFactory");

      if (indexFactory == null)
        throw new ArgumentNullException("indexFactory");

      m_HeaderFactory = headerFactory;
      m_IndexFactory = indexFactory;

      bool newFile = !File.Exists(fileName);
      m_FileStream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
      if (newFile) {
        ClearAll();
      } else {
        if (IsTransactionPending()) {
          ClearAll();
        } else {
          RecordHeader indexHeader = ReadHeader(1);
          m_StartChunks.Add(RecordContentsHeaderID, indexHeader);
          foreach (IndexEntry indexEntry in GetAllRecords<IndexEntry>(RecordContentsHeaderID, indexFactory))
          {
            m_StartChunks.Add(indexEntry.RecordType, ReadHeader(indexEntry.FilePosition));
          }
        }
      }
    }

    /// <summary>
    /// Clear out all of the recorded history
    /// </summary>
    public void ClearHistory()
    {
        ClearAll();
    }

    private void ClearAll() {
      m_StartChunks.Clear();
      m_FileStream.Seek(0, SeekOrigin.Begin);
      m_FileStream.SetLength(1);

      StartTransaction();
      RecordHeader indexHeader = new RecordHeader(IndexEntry.BINARY_SIZE, DEFAULT_CHUNK_SIZE, RecordContentsHeaderID);
      indexHeader.FilePosition = 1;
      WriteHeader(indexHeader, true);
      m_StartChunks.Add(RecordContentsHeaderID, indexHeader);
      CommitTransaction();
    }

    private void StartTransaction() {
      m_FileStream.Seek(0, SeekOrigin.Begin);
      m_FileStream.WriteByte(TRANSACTION_BYTE);
    }

    private void CommitTransaction() {
      m_FileStream.Seek(0, SeekOrigin.Begin);
      m_FileStream.WriteByte(COMMIT_BYTE);
    }

    private bool IsTransactionPending() {
      m_FileStream.Seek(0, SeekOrigin.Begin);
      return TRANSACTION_BYTE == (byte)m_FileStream.ReadByte();
    }

    private void WriteHeader(RecordHeader header, bool enlargeFile) {
      if (header == null)
        throw new ArgumentNullException("header");

      byte[] headerBytes = m_HeaderFactory.GetBytes(header);
      m_FileStream.Seek(header.FilePosition, SeekOrigin.Begin);
      m_FileStream.Write(headerBytes, 0, headerBytes.Length);
      if (enlargeFile) {
        m_FileStream.Seek(header.ChunkSize - headerBytes.Length - 1, SeekOrigin.Current);
        m_FileStream.WriteByte(0);
      }
    }

    private RecordHeader ReadHeader(long filePosition) {
      byte[] header = new byte[RecordHeader.BINARY_SIZE];
      m_FileStream.Seek(filePosition, SeekOrigin.Begin);
      for (int i = 0; i < header.Length; i += m_FileStream.Read(header, i, header.Length - i))
        ;

      RecordHeader result = m_HeaderFactory.GetObject(header);
      result.FilePosition = filePosition;
      return result;
    }

    private void SaveRecordInternal(Guid monitoringEntryID, byte[] buffer, int recordSize) {
      RecordHeader recordHeader;
        // Is there a record set associated with this monitor ID?
      if (m_StartChunks.TryGetValue(monitoringEntryID, out recordHeader)) {
          // Is there space in the current "chunk" of records?
        if (recordHeader.CanAddNewRecord()) {
            // Add the record to the current chunk
          m_FileStream.Seek(recordHeader.FilePosition + recordHeader.GetRecordOffset(), SeekOrigin.Begin);
          m_FileStream.Write(buffer, 0, buffer.Length);
          recordHeader.RecordsCount++;
          WriteHeader(recordHeader, false);
        } else {
            // Start a new chunk for this monitor ID
          RecordHeader nextChunkHeader = new RecordHeader(recordSize, DEFAULT_CHUNK_SIZE, monitoringEntryID);
          nextChunkHeader.FilePosition = m_FileStream.Length;
          recordHeader.NextRecordPosition = nextChunkHeader.FilePosition;
          WriteHeader(recordHeader, false);
          nextChunkHeader.RecordsCount++;
          WriteHeader(nextChunkHeader, true);
          nextChunkHeader.RecordsCount--;
          m_FileStream.Seek(nextChunkHeader.FilePosition + nextChunkHeader.GetRecordOffset(), SeekOrigin.Begin);
          m_FileStream.Write(buffer, 0, buffer.Length);
          nextChunkHeader.RecordsCount++;
        }
      } else {
          // Create a new record chunk for this monitor ID
        RecordHeader nextChunkHeader = new RecordHeader(recordSize, DEFAULT_CHUNK_SIZE, monitoringEntryID);
        nextChunkHeader.FilePosition = m_FileStream.Length;
        nextChunkHeader.RecordsCount++;
        WriteHeader(nextChunkHeader, true);
        nextChunkHeader.RecordsCount--;
        m_FileStream.Seek(nextChunkHeader.FilePosition + nextChunkHeader.GetRecordOffset(), SeekOrigin.Begin);
        m_FileStream.Write(buffer, 0, buffer.Length);
        nextChunkHeader.RecordsCount++;
          // Save information about the new record chunk in the table of contents.
        byte[] indexBuffer = m_IndexFactory.GetBytes(new IndexEntry(monitoringEntryID, nextChunkHeader.FilePosition));
        SaveRecordInternal(RecordContentsHeaderID, indexBuffer, IndexEntry.BINARY_SIZE);
          // Associate this new record chunk with the monitor ID
        m_StartChunks.Add(monitoringEntryID, nextChunkHeader);
      }
    }

    public void SaveRecord(Guid monitoringEntryID, byte[] buffer, int recordSize) {
      StartTransaction();
      SaveRecordInternal(monitoringEntryID, buffer, recordSize);
      CommitTransaction();
    }

      /// <summary>
      /// Get all the records for a given monitoring ID
      /// </summary>
      /// <typeparam name="T">The type of the record</typeparam>
      /// <param name="monitoringEntryID">The monitor ID to search for</param>
      /// <param name="recordSerializer">The serialization provider</param>
      /// <returns>A list of all the records that match the monitor ID</returns>
    public ReadOnlyCollection<T> GetAllRecords<T>(Guid monitoringEntryID, IBinaryFactory<T> recordSerializer) {
      if (recordSerializer == null)
        throw new ArgumentException("recordSerializer");

      List<T> items = new List<T>();
      RecordHeader header;
      if (m_StartChunks.TryGetValue(monitoringEntryID, out header))
      {
          byte[] buffer = new byte[header.RecordSize];
          while (true)
          {
              m_FileStream.Seek(header.FilePosition + RecordHeader.BINARY_SIZE, SeekOrigin.Begin);
              for (int i = 0; i < header.RecordsCount; i++)
              {
                  for (int j = 0; j < buffer.Length; j += m_FileStream.Read(buffer, j, buffer.Length - j))
                      ;
                  items.Add(recordSerializer.GetObject(buffer));
              }

              if (header.NextRecordPosition == 0)
                  break;
              header = ReadHeader(header.NextRecordPosition);
          }
      }
      return new ReadOnlyCollection<T>(items);
    }



    protected virtual void Dispose(bool disposing) {
      if (m_FileStream != null && m_FileStream.CanSeek) {
        m_FileStream.Flush();
        m_FileStream.Close();
        m_FileStream.Dispose();
        m_FileStream = null;
      }
      m_HeaderFactory = null;
      m_IndexFactory = null;
      m_StartChunks = null;
    }

    ~RecordFile() {
      Dispose(false);
    }


    #region IDisposable Members

    public void Dispose() {
      Dispose(true);
      GC.SuppressFinalize(this);
    }

    #endregion
  }
}
